TypeScript 61.9%
HTML 37.2%
SQL 0.7%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { eq } from '@/lib/db';5import { db, userBadges } from '@/lib/db';6import { publicProfile } from '@/lib/account/queries';7import { summarizePortfolio } from '@/lib/account/portfolio';8import { BADGE_DEFS } from '@/lib/account/badges';9import { Card, CardHeader, Badge, EmptyState, Delta } from '@/components/ui/primitives';10import { Donut } from '@/components/account/charts';11import { fmtMoney, fmtDate } from '@/lib/format';1213export async function generateMetadata({ params }: { params: Promise<{ handle: string }> }): Promise<Metadata> {14 const { handle } = await params;15 const p = await publicProfile(handle);16 if (!p) return { title: 'Collector not found' };17 const name = p.user.name ?? `@${p.user.handle}`;18 return { title: `${name} · Collector profile`, description: p.user.bio ?? `${name} on RareIndex`, openGraph: { title: `${name} on RareIndex`, description: p.user.bio ?? 'Collector profile', images: [`/u/${handle}/opengraph-image`] } };19}2021export default async function ProfilePage({ params }: { params: Promise<{ handle: string }> }) {22 const { handle } = await params;23 const p = await publicProfile(handle);24 if (!p) notFound();25 const total = summarizePortfolio(p.items);26 const badges = await db().select().from(userBadges).where(eq(userBadges.userId, p.user.id));27 return (28 <div className="mx-auto max-w-4xl">29 <header className="flex flex-col gap-4 py-6 sm:flex-row sm:items-center">30 {p.user.avatarUrl ? (31 // eslint-disable-next-line @next/next/no-img-element32 <img src={p.user.avatarUrl} alt="" className="h-20 w-20 rounded-full border border-border object-cover" />33 ) : (34 <span className="inline-flex h-20 w-20 items-center justify-center rounded-full bg-accent text-2xl font-semibold text-accent-fg">{(p.user.name ?? p.user.handle ?? '?').slice(0, 1).toUpperCase()}</span>35 )}36 <div className="min-w-0 flex-1">37 <h1 className="text-2xl font-semibold tracking-tight">{p.user.name ?? `@${p.user.handle}`}</h1>38 <p className="text-sm text-muted">39 @{p.user.handle} · member since {fmtDate(p.user.createdAt, { month: 'long' })}40 </p>41 {p.user.bio ? <p className="mt-2 max-w-xl text-sm">{p.user.bio}</p> : null}42 {badges.length ? (43 <div className="mt-3 flex flex-wrap gap-1.5">44 {badges.map((b) => (45 <Badge key={b.badge} tone={BADGE_DEFS[b.badge]?.tone ?? 'neutral'}>46 {BADGE_DEFS[b.badge]?.label ?? b.badge}47 </Badge>48 ))}49 </div>50 ) : null}51 </div>52 <div className="text-right">53 <p className="text-[11px] uppercase tracking-wider text-subtle">Public collection value</p>54 <p className="num text-2xl font-semibold">{total.valuedCount ? fmtMoney(total.valueUsd) : '—'}</p>55 <p className="text-xs text-muted">56 {total.itemCount} item{total.itemCount === 1 ? '' : 's'} across {p.collections.length} public collection{p.collections.length === 1 ? '' : 's'}57 </p>58 </div>59 </header>60 {p.collections.length === 0 ? (61 <Card>62 <EmptyState title="No public collections" description="This collector has not shared any collection yet." />63 </Card>64 ) : (65 <div className="grid gap-4 md:grid-cols-[minmax(0,2fr)_minmax(0,1fr)]">66 <div className="space-y-3">67 {p.collections.map(({ collection: c, summary: s }) => (68 <Link key={c.id} href={`/u/${handle}/${c.publicSlug}`} className="card flex items-center justify-between gap-3 p-4 hover:border-border-strong">69 <div className="min-w-0">70 <p className="truncate text-sm font-semibold">{c.name}</p>71 <p className="text-xs text-muted">{c.description ?? `${s.itemCount} items`}</p>72 </div>73 <div className="text-right">74 <p className="num text-base font-semibold">{s.valuedCount ? fmtMoney(s.valueUsd) : '—'}</p>75 <Delta value={s.returnPct} className="text-xs" />76 </div>77 </Link>78 ))}79 </div>80 <Card>81 <CardHeader title="Allocation" />82 <div className="p-4">83 <Donut data={total.allocationByFamily.map((b) => ({ label: b.label, value: b.valueUsd }))} size={110} />84 </div>85 </Card>86 </div>87 )}88 <p className="mt-6 text-[11px] text-subtle">Values are RareIndex Valuation estimates with confidence levels; purchase prices are never shown publicly. RareIndex does not authenticate items.</p>89 </div>90 );91}92